﻿namespace LiteBlog.XmlProviders
{
    using System;
    using System.Collections.Specialized;
    using System.Configuration.Provider;
    using System.Globalization;
    using System.Web;
    using System.Web.Security;
    using System.Xml;

    using LiteBlog.Common;
    using LiteBlog.Common.Components;

    public class XmlMembershipProvider : MembershipProvider
    {
        private const string DateFormat = "dd/MM/yyyy hh:mm tt";

        private XmlDocument _doc = new XmlDocument();
        private string _name = string.Empty;
        private string _path = string.Empty;
        private TimeZoneInfo _tzi;
        private bool firstLoad = true;

        
        public override string ApplicationName { get; set; }
        
        public override bool EnablePasswordReset
        {
            get { return false; }
        }

        public override bool EnablePasswordRetrieval
        {
            get  { return false; }
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { return 5; }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return 0; }
        }

        public override int MinRequiredPasswordLength
        {
            get { return 0; }
        }

        public override int PasswordAttemptWindow
        {
            get { return 15; }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { return MembershipPasswordFormat.Clear; }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { return string.Empty; }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { return false; }
        }

        public override bool RequiresUniqueEmail
        {
            get { return true; }
        }

        
        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, newPassword, false);
            this.OnValidatingPassword(args);
            if (args.Cancel)
            {
                if (args.FailureInformation != null)
                {
                    throw args.FailureInformation;
                }
                else
                {
                    throw new MembershipPasswordException("Invalid password format");
                }
            }

            lock (this)
            {
                string xpath = @"/Users/User[@Name=""{0}""]";
                XmlNode userNode = this._doc.SelectSingleNode(string.Format(xpath, username));
                if (userNode == null)
                {
                    return false;
                }

                XmlNode pwdNode = userNode.SelectSingleNode("Password");
                string password = this.DecryptPassword(pwdNode.InnerText);
                if (oldPassword != password)
                {
                    return false;
                }

                pwdNode.InnerText = this.EncryptPassword(newPassword);

                userNode.SelectSingleNode("LastActivityTime").InnerText = GetLocalTime();
                userNode.SelectSingleNode("LastPasswordTime").InnerText = GetLocalTime();

                this._doc.Save(this._path);
            }

            return true;
        }

        public override bool ChangePasswordQuestionAndAnswer(
            string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            return false;
        }

        public override MembershipUser CreateUser(
            string username,
            string password,
            string email,
            string passwordQuestion,
            string passwordAnswer,
            bool isApproved,
            object providerUserKey,
            out MembershipCreateStatus status)
        {
            MembershipUser user = null;
            MembershipUser user2 = null;

            ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, password, true);
            this.OnValidatingPassword(args);
            if (args.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return user;
            }

            if (providerUserKey == null)
            {
                providerUserKey = Guid.NewGuid();
            }
            else if (providerUserKey.GetType() != typeof(Guid))
            {
                status = MembershipCreateStatus.InvalidProviderUserKey;
                return user;
            }
            else
            {
                user2 = this.GetUser(providerUserKey, false);
                if (user2 != null)
                {
                    status = MembershipCreateStatus.DuplicateProviderUserKey;
                    return user;
                }
            }

            user2 = this.GetUser(username, false);
            if (user2 != null)
            {
                status = MembershipCreateStatus.DuplicateUserName;
                return user;
            }

            if (this.RequiresUniqueEmail)
            {
                if (this.GetUserNameByEmail(email) != string.Empty)
                {
                    status = MembershipCreateStatus.DuplicateEmail;
                    return user;
                }
            }

            lock (this)
            {
                XmlElement userElem = this._doc.CreateElement("User");
                XmlAttribute keyAttr = this._doc.CreateAttribute("PKey");
                XmlAttribute nameAttr = this._doc.CreateAttribute("Name");
                XmlAttribute emailAttr = this._doc.CreateAttribute("Email");

                keyAttr.Value = ((Guid)providerUserKey).ToString();
                nameAttr.Value = username;
                emailAttr.Value = email;

                userElem.Attributes.Append(keyAttr);
                userElem.Attributes.Append(nameAttr);
                userElem.Attributes.Append(emailAttr);

                XmlElement pwdElem = this._doc.CreateElement("Password");
                pwdElem.InnerText = this.EncryptPassword(password);
                userElem.AppendChild(pwdElem);

                XmlElement lockElem = this._doc.CreateElement("Locked");
                lockElem.InnerText = false.ToString();
                userElem.AppendChild(lockElem);

                XmlElement createdElem = this._doc.CreateElement("CreatedTime");
                createdElem.InnerText = GetLocalTime();
                userElem.AppendChild(createdElem);

                XmlElement loginElem = this._doc.CreateElement("LastLoginTime");
                loginElem.InnerText = GetLocalTime();
                userElem.AppendChild(loginElem);

                XmlElement actElem = this._doc.CreateElement("LastActivityTime");
                actElem.InnerText = GetLocalTime();
                userElem.AppendChild(actElem);

                XmlElement lastPwdElem = this._doc.CreateElement("LastPasswordTime");
                lastPwdElem.InnerText = GetLocalTime();
                userElem.AppendChild(lastPwdElem);

                XmlElement lastLockElem = this._doc.CreateElement("LastLockedTime");
                lastLockElem.InnerText = GetLocalTime();
                userElem.AppendChild(lastLockElem);

                this._doc.DocumentElement.AppendChild(userElem);
                this._doc.Save(this._path);
            }

            status = MembershipCreateStatus.Success;
            user = new MembershipUser(
                this._name,
                username,
                providerUserKey,
                email,
                string.Empty,
                string.Empty,
                true,
                false,
                LocalNow,
                LocalNow,
                LocalNow,
                LocalNow,
                LocalNow);
            return user;
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            string xpath = @"/Users/User[@Name=""{0}""]";
            lock (this)
            {
                XmlNode userNode = this._doc.SelectSingleNode(string.Format(xpath, username));
                if (userNode == null)
                {
                    throw new ProviderException("User not found");
                }

                this._doc.DocumentElement.RemoveChild(userNode);
                this._doc.Save(this._path);
            }

            return true;
        }

        public override MembershipUserCollection FindUsersByEmail(
            string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            string xpath = @"/Users/User[Contains(@Email,""{0}"")]";
            xpath = string.Format(xpath, emailToMatch);
            XmlNodeList userList = this._doc.SelectNodes(xpath);
            return this.GetUsers(userList, pageIndex, pageSize, out totalRecords);
        }

        public override MembershipUserCollection FindUsersByName(
            string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            string xpath = @"/Users/User[Contains(@Name,""{0}"")]";
            xpath = string.Format(xpath, usernameToMatch);
            XmlNodeList userList = this._doc.SelectNodes(xpath);
            return this.GetUsers(userList, pageIndex, pageSize, out totalRecords);
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            XmlNodeList userList = this._doc.SelectNodes("/Users/User");
            return this.GetUsers(userList, pageIndex, pageSize, out totalRecords);
        }

        public override int GetNumberOfUsersOnline()
        {
            DateTime onlineTime = LocalNow.AddMinutes(-Membership.UserIsOnlineTimeWindow);
            XmlNodeList userList = this._doc.SelectNodes("/Users/User");
            int onlineCount = 0;
            foreach (XmlNode user in userList)
            {
                DateTime lastActTime = DateTime.MinValue;
                DateTime.TryParseExact(
                    user.SelectSingleNode("LastActivityTime").InnerText,
                    DateFormat,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.None,
                    out lastActTime);
                if (lastActTime > onlineTime)
                {
                    onlineCount++;
                }
            }

            return onlineCount;
        }

        public override string GetPassword(string username, string answer)
        {
            string xpath = @"/Users/User[@Name=""{0}""]";
            XmlNode userNode = this._doc.SelectSingleNode(string.Format(xpath, username));
            if (userNode == null)
            {
                return string.Empty;
            }

            XmlNode pwdNode = userNode.SelectSingleNode("Password");
            return this.DecryptPassword(pwdNode.InnerText);
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            string xpath = @"/Users/User[@PKey=""{0}""]";
            xpath = string.Format(xpath, providerUserKey.ToString());
            XmlNode userNode = this._doc.SelectSingleNode(xpath);
            MembershipUser user = this.GetUserFromUserNode(userNode);
            if (userIsOnline)
            {
                this.SetLastActivity(user.UserName);
            }

            return user;
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            string xpath = @"/Users/User[@Name=""{0}""]";
            xpath = string.Format(xpath, username);
            XmlNode userNode = this._doc.SelectSingleNode(xpath);
            MembershipUser user = this.GetUserFromUserNode(userNode);
            if (userIsOnline)
            {
                this.SetLastActivity(user.UserName);
            }

            return user;
        }

        public override string GetUserNameByEmail(string email)
        {
            string xpath = @"/Users/User[@Email=""{0}""]";
            xpath = string.Format(xpath, email);
            XmlNode userNode = this._doc.SelectSingleNode(xpath);
            if (userNode == null)
            {
                return string.Empty;
            }

            return userNode.Attributes["Name"].Value;
        }

        public override void Initialize(string name, NameValueCollection config)
        {
            if (string.IsNullOrEmpty(name))
            {
                name = "Xml Membership Provider";
            }

            if (this.firstLoad)
            {
                base.Initialize(name, config);
            }

            this._name = name;

            // this._path = HttpContext.Current.Server.MapPath("~/App_data/User.xml");
            this._path = string.Format("{0}User.xml", config["DataPath"]);

            try
            {
                _doc.Load(this._path);
                ApplicationName = config["AppName"];
                _tzi = TimeZoneInfo.FindSystemTimeZoneById(config["Timezone"]);
            }
            catch (Exception ex)
            {
                if (!this.firstLoad)
                {
                    Logger.Log("Membership data not loaded");
                    throw new ProviderException("Membership data not loaded", ex);
                }
                else
                {
                    this.firstLoad = false;
                }
            }

            // this._appName = HttpContext.Current.Request.ApplicationPath.Replace("/", string.Empty);

            // Ignoring all the config values and supply with defaults
            // Question / Answer, Password reset / retrieval, Password related features are all disabled.
        }

        public override string ResetPassword(string username, string answer)
        {
            return string.Empty;
        }

        public override bool UnlockUser(string userName)
        {
            string xpath = @"/Users/User[@Name=""{0}""]";
            lock (this)
            {
                XmlNode userNode = this._doc.SelectSingleNode(string.Format(xpath, userName));
                if (userNode == null)
                {
                    throw new ProviderException("User not found");
                }

                userNode.SelectSingleNode("Locked").InnerText = false.ToString();
                userNode.SelectSingleNode("LastLockedTime").InnerText = GetLocalTime();

                this._doc.Save(this._path);
            }

            return true;
        }


        public override void UpdateUser(MembershipUser user)
        {
            lock (this)
            {
                string xpath = @"/Users/User[@Name=""{0}""]";
                XmlNode userNode = this._doc.SelectSingleNode(string.Format(xpath, user.UserName));
                if (userNode == null)
                {
                    throw new ProviderException("User not found");
                }

                // Update the user with email or display name
                userNode.Attributes["Email"].Value = user.Email;
                var customUser = user as CustomMembershipUser;
                if (customUser != null)
                {
                    var displayNameAttribute = userNode.Attributes["DisplayName"];
                    if (displayNameAttribute == null)
                    {
                        displayNameAttribute = _doc.CreateAttribute("DisplayName");
                        userNode.Attributes.Append(displayNameAttribute);
                    }
                    displayNameAttribute.Value = customUser.DisplayName;

                    var urlAttribute = userNode.Attributes["Url"];
                    if (urlAttribute == null)
                    {
                        urlAttribute = _doc.CreateAttribute("Url");
                        userNode.Attributes.Append(urlAttribute);
                    }
                    urlAttribute.Value = customUser.Url;

                    var primaryAttribute = userNode.Attributes["IsPrimary"];
                    if (primaryAttribute == null)
                    {
                        primaryAttribute = _doc.CreateAttribute("IsPrimary");
                        userNode.Attributes.Append(primaryAttribute);
                    }
                    primaryAttribute.Value = customUser.IsPrimary.ToString();

                    var enabledAttribute = userNode.Attributes["Enabled"];
                    if (enabledAttribute == null)
                    {
                        enabledAttribute = _doc.CreateAttribute("Enabled");
                        userNode.Attributes.Append(enabledAttribute);
                    }
                    enabledAttribute.Value = customUser.Enabled.ToString();
                }
                this._doc.Save(this._path);
            }
        }

        public override bool ValidateUser(string username, string password)
        {
            lock (this)
            {
                string xpath = @"/Users/User[@Name=""{0}""]";
                XmlNode userNode = this._doc.SelectSingleNode(string.Format(xpath, username));

                if (userNode == null)
                {
                    return false;
                }

                int attempts = -1; // HttpContext.Current is null
                if (HttpContext.Current != null)
                {
                    if (HttpContext.Current.Session["Attempts"] == null)
                    {
                        HttpContext.Current.Session["Attempts"] = 0;
                    }

                    attempts = (int)HttpContext.Current.Session["Attempts"];
                }

                string password2 = this.DecryptPassword(userNode.SelectSingleNode("Password").InnerText);
                bool locked = true;
                bool.TryParse(userNode.SelectSingleNode("Locked").InnerText, out locked);
                if (locked)
                {
                    return false;
                }

                if (password2 != password)
                {
                    if (attempts != -1)
                    {
                        attempts++;
                        HttpContext.Current.Session["Attempts"] = attempts;
                        if (attempts > this.MaxInvalidPasswordAttempts)
                        {
                            this.LockUser(username);
                        }
                    }

                    return false;
                }

                if (attempts != -1)
                {
                    HttpContext.Current.Session["Attempts"] = 0;
                }

                userNode.SelectSingleNode("LastActivityTime").InnerText = GetLocalTime();
                this._doc.Save(this._path);
            }

            return true;
        }

        private DateTime LocalNow
        {
            get
            {
                return TimeZoneInfo.ConvertTime(DateTime.Now, _tzi);
            }
        }

        private string GetLocalTime()
        {
            return LocalNow.ToString(DateFormat, CultureInfo.InvariantCulture);
        }

        private string DecryptPassword(string password)
        {
            // encrypt / decrypt using own algorithm
            return password;
        }

        private string EncryptPassword(string password)
        {
            // encrypt / decrypt using your own algorithm
            return password;
        }

        private MembershipUser GetUserFromUserNode(XmlNode userNode)
        {
            CustomMembershipUser user = null;
            if (userNode == null)
            {
                return user;
            }

            try
            {
                Guid pKey;
                Guid.TryParse(userNode.Attributes["PKey"].Value, out pKey);

                string userName = userNode.Attributes["Name"].Value;
                string email = userNode.Attributes["Email"].Value;
                string displayName = userNode.Attributes["DisplayName"] != null ? userNode.Attributes["DisplayName"].Value : string.Empty;
                string url = userNode.Attributes["Url"] != null ? userNode.Attributes["Url"].Value : string.Empty;
                bool isPrimary = userNode.Attributes["IsPrimary"] != null ? bool.Parse(userNode.Attributes["IsPrimary"].Value) : false;
                bool enabled = userNode.Attributes["Enabled"] != null ? bool.Parse(userNode.Attributes["Enabled"].Value) : true;
                bool isLocked = false;
                bool.TryParse(userNode.SelectSingleNode("Locked").InnerText, out isLocked);
                DateTime creationTime, lastLoginTime, lastActivityTime, lastPwdTime, lastLockedTime;
                DateTime.TryParseExact(
                    userNode.SelectSingleNode("CreatedTime").InnerText,
                    DateFormat,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.None,
                    out creationTime);
                DateTime.TryParseExact(
                    userNode.SelectSingleNode("LastLoginTime").InnerText,
                    DateFormat,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.None,
                    out lastLoginTime);
                DateTime.TryParseExact(
                    userNode.SelectSingleNode("LastActivityTime").InnerText,
                    DateFormat,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.None,
                    out lastActivityTime);
                DateTime.TryParseExact(
                    userNode.SelectSingleNode("LastPasswordTime").InnerText,
                    DateFormat,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.None,
                    out lastPwdTime);
                DateTime.TryParseExact(
                    userNode.SelectSingleNode("LastLockedTime").InnerText,
                    DateFormat,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.None,
                    out lastLockedTime);

                user = new CustomMembershipUser(
                    this._name,
                    userName,
                    pKey,
                    email,
                    displayName,
                    isLocked,
                    creationTime,
                    lastLoginTime,
                    lastActivityTime,
                    lastPwdTime,
                    lastLockedTime);
                user.IsPrimary = isPrimary;
                user.Enabled = enabled;
                user.Url = url;
            }
            catch (Exception ex)
            {
                throw new ProviderException("The user repository has data issues", ex);
            }

            return user;
        }

        private MembershipUserCollection GetUsers(
            XmlNodeList userList, int pageIndex, int pageSize, out int totalRecords)
        {
            if (pageIndex < 0)
            {
                throw new ProviderException("Invalid page params");
            }

            if (pageSize < 1)
            {
                throw new ProviderException("Invalid page size");
            }

            MembershipUserCollection users = new MembershipUserCollection();
            totalRecords = 0;

            if (userList == null)
            {
                return users;
            }

            totalRecords = userList.Count;
            int startIndex = pageIndex * pageSize;
            int endIndex = startIndex + pageSize - 1;

            totalRecords = userList.Count;

            if (totalRecords < (startIndex + 1))
            {
                return users;
            }

            if (endIndex > totalRecords - 1)
            {
                endIndex = totalRecords - 1;
            }

            for (int curIndex = startIndex; curIndex <= endIndex; curIndex++)
            {
                MembershipUser user = this.GetUserFromUserNode(userList[curIndex]);
                users.Add(user);
            }

            return users;
        }

        private void LockUser(string userName)
        {
            string xpath = @"/Users/User[@Name=""{0}""]";
            XmlNode userNode = this._doc.SelectSingleNode(string.Format(xpath, userName));
            if (userNode == null)
            {
                throw new ProviderException("User not found");
            }

            userNode.SelectSingleNode("Locked").InnerText = true.ToString();
            this._doc.Save(this._path);
        }

        private void SetLastActivity(string userName)
        {
            lock (this)
            {
                string xpath = @"/Users/User[@Name=""{0}""]";
                xpath = string.Format(xpath, userName);
                XmlNode userNode = this._doc.SelectSingleNode(xpath);
                try
                {
                    XmlNode actNode = userNode.SelectSingleNode("LastActivityTime");
                    actNode.InnerText = GetLocalTime();
                }
                catch (Exception ex)
                {
                    throw new ProviderException("XML is not in right format", ex);
                }

                this._doc.Save(this._path);
            }
        }
    }
}